﻿using System;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
using System.Windows;

namespace HandyControl.Interactivity;

public abstract class EventTriggerBase : TriggerBase
{
    public static readonly DependencyProperty SourceNameProperty = DependencyProperty.Register("SourceName",
        typeof(string), typeof(EventTriggerBase), new PropertyMetadata(OnSourceNameChanged));

    public static readonly DependencyProperty SourceObjectProperty = DependencyProperty.Register("SourceObject",
        typeof(object), typeof(EventTriggerBase), new PropertyMetadata(OnSourceObjectChanged));

    private MethodInfo _eventHandlerMethodInfo;

    internal EventTriggerBase(Type sourceTypeConstraint) : base(typeof(DependencyObject))
    {
        SourceTypeConstraint = sourceTypeConstraint;
        SourceNameResolver = new NameResolver();
        RegisterSourceChanged();
    }

    protected sealed override Type AssociatedObjectTypeConstraint
    {
        get
        {
            if (TypeDescriptor.GetAttributes(GetType())[typeof(TypeConstraintAttribute)] is TypeConstraintAttribute attribute)
                return attribute.Constraint;
            return typeof(DependencyObject);
        }
    }

    private bool IsLoadedRegistered { get; set; }

    private bool IsSourceChangedRegistered { get; set; }

    private bool IsSourceNameSet
    {
        get
        {
            if (string.IsNullOrEmpty(SourceName))
                return ReadLocalValue(SourceNameProperty) != DependencyProperty.UnsetValue;
            return true;
        }
    }

    public object Source
    {
        get
        {
            object associatedObject = AssociatedObject;
            if (SourceObject != null)
                return SourceObject;
            if (IsSourceNameSet)
            {
                associatedObject = SourceNameResolver.Object;
                if (associatedObject != null && !SourceTypeConstraint.IsInstanceOfType(associatedObject))
                    throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
                        ExceptionStringTable.RetargetedTypeConstraintViolatedExceptionMessage, GetType().Name,
                        associatedObject.GetType(), SourceTypeConstraint, "Source"));
            }
            return associatedObject;
        }
    }

    public string SourceName
    {
        get =>
            (string) GetValue(SourceNameProperty);
        set => SetValue(SourceNameProperty, value);
    }

    private NameResolver SourceNameResolver { get; }

    public object SourceObject
    {
        get =>
            GetValue(SourceObjectProperty);
        set => SetValue(SourceObjectProperty, value);
    }

    protected Type SourceTypeConstraint { get; }

    protected abstract string GetEventName();

    private static bool IsValidEvent(EventInfo eventInfo)
    {
        var eventHandlerType = eventInfo.EventHandlerType;
        if (!typeof(Delegate).IsAssignableFrom(eventInfo.EventHandlerType))
            return false;
        var parameters = eventHandlerType.GetMethod("Invoke")?.GetParameters();
        return parameters != null && parameters.Length == 2 && typeof(object).IsAssignableFrom(parameters[0].ParameterType) && typeof(EventArgs).IsAssignableFrom(parameters[1].ParameterType);
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        var associatedObject = AssociatedObject;
        var behavior = associatedObject as Behavior;
        var element = associatedObject as FrameworkElement;
        RegisterSourceChanged();
        if (behavior != null)
        {
            behavior.AssociatedObjectChanged += OnBehaviorHostChanged;
        }
        else if (SourceObject != null || element == null)
        {
            try
            {
                OnSourceChanged(null, Source);
            }
            catch (InvalidOperationException)
            {
            }
        }
        else
        {
            SourceNameResolver.NameScopeReferenceElement = element;
        }
        if (string.Compare(GetEventName(), "Loaded", StringComparison.Ordinal) == 0 && element != null &&
            !Interaction.IsElementLoaded(element))
            RegisterLoaded(element);
    }

    private void OnBehaviorHostChanged(object sender, EventArgs e)
    {
        SourceNameResolver.NameScopeReferenceElement =
            ((IAttachedObject) sender).AssociatedObject as FrameworkElement;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        var associatedObject = AssociatedObject as Behavior;
        var associatedElement = AssociatedObject as FrameworkElement;
        try
        {
            OnSourceChanged(Source, null);
        }
        catch (InvalidOperationException)
        {
        }
        UnregisterSourceChanged();
        if (associatedObject != null)
            associatedObject.AssociatedObjectChanged -= OnBehaviorHostChanged;
        SourceNameResolver.NameScopeReferenceElement = null;
        if (string.Compare(GetEventName(), "Loaded", StringComparison.Ordinal) == 0 && associatedElement != null)
            UnregisterLoaded(associatedElement);
    }

    protected virtual void OnEvent(EventArgs eventArgs)
    {
        InvokeActions(eventArgs);
    }

    private void OnEventImpl(object sender, EventArgs eventArgs)
    {
        OnEvent(eventArgs);
    }

    internal void OnEventNameChanged(string oldEventName, string newEventName)
    {
        if (AssociatedObject != null)
        {
            var source = Source as FrameworkElement;
            if (source != null && string.Compare(oldEventName, "Loaded", StringComparison.Ordinal) == 0)
                UnregisterLoaded(source);
            else if (!string.IsNullOrEmpty(oldEventName))
                UnregisterEvent(Source, oldEventName);
            if (source != null && string.Compare(newEventName, "Loaded", StringComparison.Ordinal) == 0)
                RegisterLoaded(source);
            else if (!string.IsNullOrEmpty(newEventName))
                RegisterEvent(Source, newEventName);
        }
    }

    private void OnSourceChanged(object oldSource, object newSource)
    {
        if (AssociatedObject != null)
            OnSourceChangedImpl(oldSource, newSource);
    }

    internal virtual void OnSourceChangedImpl(object oldSource, object newSource)
    {
        if (!string.IsNullOrEmpty(GetEventName()) &&
            string.Compare(GetEventName(), "Loaded", StringComparison.Ordinal) != 0)
        {
            if (oldSource != null && SourceTypeConstraint.IsInstanceOfType(oldSource))
                UnregisterEvent(oldSource, GetEventName());
            if (newSource != null && SourceTypeConstraint.IsInstanceOfType(newSource))
                RegisterEvent(newSource, GetEventName());
        }
    }

    private static void OnSourceNameChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        var base2 = (EventTriggerBase) obj;
        base2.SourceNameResolver.Name = (string) args.NewValue;
    }

    private void OnSourceNameResolverElementChanged(object sender, NameResolvedEventArgs e)
    {
        if (SourceObject == null)
            OnSourceChanged(e.OldObject, e.NewObject);
    }

    private static void OnSourceObjectChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        var base2 = (EventTriggerBase) obj;
        object newSource = base2.SourceNameResolver.Object;
        if (args.NewValue == null)
        {
            base2.OnSourceChanged(args.OldValue, newSource);
        }
        else
        {
            if (args.OldValue == null && newSource != null)
                base2.UnregisterEvent(newSource, base2.GetEventName());
            base2.OnSourceChanged(args.OldValue, args.NewValue);
        }
    }

    private void RegisterEvent(object obj, string eventName)
    {
        var eventInfo = obj.GetType().GetEvent(eventName);
        if (eventInfo == null)
        {
            if (SourceObject != null)
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                    ExceptionStringTable.EventTriggerCannotFindEventNameExceptionMessage,
                    new object[] { eventName, obj.GetType().Name }));
        }
        else if (!IsValidEvent(eventInfo))
        {
            if (SourceObject != null)
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                    ExceptionStringTable.EventTriggerBaseInvalidEventExceptionMessage,
                    new object[] { eventName, obj.GetType().Name }));
        }
        else
        {
            _eventHandlerMethodInfo =
                typeof(EventTriggerBase).GetMethod("OnEventImpl", BindingFlags.NonPublic | BindingFlags.Instance);
            eventInfo.AddEventHandler(obj,
                Delegate.CreateDelegate(eventInfo.EventHandlerType, this, _eventHandlerMethodInfo ?? throw new InvalidOperationException()));
        }
    }

    private void RegisterLoaded(FrameworkElement associatedElement)
    {
        if (!IsLoadedRegistered && associatedElement != null)
        {
            associatedElement.Loaded += OnEventImpl;
            IsLoadedRegistered = true;
        }
    }

    private void RegisterSourceChanged()
    {
        if (!IsSourceChangedRegistered)
        {
            SourceNameResolver.ResolvedElementChanged += OnSourceNameResolverElementChanged;
            IsSourceChangedRegistered = true;
        }
    }

    private void UnregisterEvent(object obj, string eventName)
    {
        if (string.Compare(eventName, "Loaded", StringComparison.Ordinal) == 0)
        {
            if (obj is FrameworkElement associatedElement)
                UnregisterLoaded(associatedElement);
        }
        else
        {
            UnregisterEventImpl(obj, eventName);
        }
    }

    private void UnregisterEventImpl(object obj, string eventName)
    {
        var type = obj.GetType();
        if (_eventHandlerMethodInfo != null)
        {
            var info = type.GetEvent(eventName);
            info.RemoveEventHandler(obj,
                Delegate.CreateDelegate(info.EventHandlerType, this, _eventHandlerMethodInfo));
            _eventHandlerMethodInfo = null;
        }
    }

    private void UnregisterLoaded(FrameworkElement associatedElement)
    {
        if (IsLoadedRegistered && associatedElement != null)
        {
            associatedElement.Loaded -= OnEventImpl;
            IsLoadedRegistered = false;
        }
    }

    private void UnregisterSourceChanged()
    {
        if (IsSourceChangedRegistered)
        {
            SourceNameResolver.ResolvedElementChanged -= OnSourceNameResolverElementChanged;
            IsSourceChangedRegistered = false;
        }
    }
}
